page.tsx 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. "use client";
  2. import { useEffect, useState } from "react";
  3. import { useTranslations } from "next-intl";
  4. import { Link } from "@/i18n/navigation";
  5. import { useAuth } from "@/providers/auth-provider";
  6. import { FileText, ArrowLeft, Loader2 } from "lucide-react";
  7. import {
  8. cancelOrder,
  9. fetchOrderList,
  10. getOrderStatusLabel,
  11. type OrderRecord,
  12. } from "@/lib/order-api";
  13. import { cn } from "@/lib/utils";
  14. import { AccountLoginRequired } from "@/components/auth/account-login-required";
  15. const PAGE_SIZE = 10;
  16. export default function AccountOrdersPage() {
  17. const t = useTranslations("account");
  18. const { user, isReady } = useAuth();
  19. const [orders, setOrders] = useState<OrderRecord[]>([]);
  20. const [loading, setLoading] = useState(false);
  21. const [error, setError] = useState<string | null>(null);
  22. const [page, setPage] = useState(1);
  23. const [total, setTotal] = useState(0);
  24. const [cancelTarget, setCancelTarget] = useState<OrderRecord | null>(null);
  25. const [cancelLoading, setCancelLoading] = useState(false);
  26. useEffect(() => {
  27. if (!user) return;
  28. let cancelled = false;
  29. async function loadOrders() {
  30. setLoading(true);
  31. setError(null);
  32. try {
  33. const res = await fetchOrderList({ current: page, row: PAGE_SIZE });
  34. if (cancelled) return;
  35. setOrders(res.list);
  36. setTotal(res.page.total);
  37. } catch (e) {
  38. if (cancelled) return;
  39. const err = e as Error;
  40. setError(err.message || "订单加载失败,请稍后重试。");
  41. setOrders([]);
  42. } finally {
  43. if (!cancelled) setLoading(false);
  44. }
  45. }
  46. void loadOrders();
  47. return () => { cancelled = true; };
  48. }, [user, page]);
  49. if (!isReady) return <div className="min-h-screen bg-[#050b14] flex items-center justify-center text-slate-500"><Loader2 className="animate-spin" /></div>;
  50. if (!user) {
  51. return <AccountLoginRequired />;
  52. }
  53. const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
  54. async function handleConfirmCancel() {
  55. if (!cancelTarget) return;
  56. try {
  57. setCancelLoading(true);
  58. await cancelOrder(cancelTarget.id);
  59. const res = await fetchOrderList({ current: page, row: PAGE_SIZE });
  60. setOrders(res.list);
  61. setTotal(res.page.total);
  62. setCancelTarget(null);
  63. } catch (e) {
  64. setError((e as Error).message || "取消订单失败,请稍后重试。");
  65. } finally {
  66. setCancelLoading(false);
  67. }
  68. }
  69. function getStatusStyle(status: number | string) {
  70. const s = String(status);
  71. if (s === "2" || s === "3") return "text-emerald-400 border-emerald-400/30 bg-emerald-400/10";
  72. if (s === "4" || s === "5") return "text-rose-400 border-rose-400/30 bg-rose-400/10";
  73. return "text-amber-400 border-amber-400/30 bg-amber-400/10";
  74. }
  75. return (
  76. <div className="min-h-screen bg-[#050b14] pb-24 text-slate-300 font-sans relative">
  77. <div className="pointer-events-none fixed inset-0 z-0">
  78. <div className="absolute left-1/4 top-0 h-[500px] w-[500px] rounded-full bg-blue-900/10 blur-[120px]" />
  79. <div className="absolute right-1/4 bottom-0 h-[500px] w-[500px] rounded-full bg-[#b89458]/5 blur-[120px]" />
  80. </div>
  81. <div className="site-container relative z-10 pt-16">
  82. <div className="flex items-center justify-between mb-8">
  83. <div>
  84. <h1 className="font-serif text-3xl font-bold text-white">全部订单</h1>
  85. <p className="mt-2 text-sm text-slate-400">管理您的所有购买记录与账单明细</p>
  86. </div>
  87. <Link href="/account" className="flex items-center gap-2 rounded-full border border-white/10 bg-white/5 px-5 py-2.5 text-sm font-semibold text-slate-300 transition hover:bg-white/10 hover:text-white backdrop-blur-md">
  88. <ArrowLeft size={16} /> 返回控制中心
  89. </Link>
  90. </div>
  91. <section className="rounded-[2.5rem] border border-white/10 bg-white/5 p-6 md:p-10 backdrop-blur-2xl shadow-2xl">
  92. {loading ? <div className="py-20 flex justify-center text-slate-500"><Loader2 className="animate-spin h-8 w-8" /></div> : null}
  93. {error ? <div className="mb-6 rounded-2xl border border-rose-500/20 bg-rose-500/10 p-4 text-sm text-rose-400">{error}</div> : null}
  94. {!loading && !error && orders.length === 0 ? (
  95. <div className="py-20 flex flex-col items-center justify-center rounded-[2rem] border border-dashed border-white/10 bg-white/5 text-slate-500">
  96. <FileText size={48} className="mb-4 opacity-50" />
  97. <p className="text-sm">{t("noOrders")}</p>
  98. </div>
  99. ) : null}
  100. {!loading && !error && orders.length > 0 ? (
  101. <div className="space-y-4">
  102. {orders.map((o) => (
  103. <div key={o.serial} className="group rounded-[1.5rem] border border-white/5 bg-white/5 p-6 transition-all hover:bg-white/10">
  104. <div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
  105. <div className="flex items-start gap-5">
  106. <div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-2xl bg-white/5 text-[#f3deae]">
  107. <FileText size={22} />
  108. </div>
  109. <div>
  110. <p className="text-base font-bold text-white group-hover:text-[#f3deae] transition-colors">{o.details}</p>
  111. <div className="mt-2 flex flex-wrap items-center gap-x-4 gap-y-2 text-xs font-medium text-slate-500">
  112. <span>NO: {o.serial}</span>
  113. <span className="hidden sm:inline">•</span>
  114. <span>创建: {o.addTime || "-"}</span>
  115. {o.payTime && <><span className="hidden sm:inline">•</span><span>支付: {o.payTime}</span></>}
  116. </div>
  117. </div>
  118. </div>
  119. <div className="flex items-center justify-between md:justify-end gap-6 md:border-l md:border-white/5 md:pl-6">
  120. <div className="text-right">
  121. <p className="text-xl font-bold text-white tracking-tight">${o.amount}</p>
  122. <span className={cn("inline-block mt-1.5 rounded-full border px-3 py-1 text-[10px] font-bold uppercase tracking-widest", getStatusStyle(o.status))}>
  123. {getOrderStatusLabel(o.status)}
  124. </span>
  125. </div>
  126. {o.status === 1 && (
  127. <button onClick={() => setCancelTarget(o)} className="rounded-xl border border-rose-500/30 bg-rose-500/10 px-4 py-2 text-xs font-bold text-rose-400 transition hover:bg-rose-500/20">取消</button>
  128. )}
  129. </div>
  130. </div>
  131. </div>
  132. ))}
  133. </div>
  134. ) : null}
  135. {/* 分页 */}
  136. {!loading && !error && totalPages > 1 && (
  137. <div className="mt-10 flex items-center justify-center gap-4 border-t border-white/5 pt-8">
  138. <button onClick={() => setPage(p => Math.max(1, p - 1))} disabled={page <= 1} className="rounded-xl border border-white/10 bg-white/5 px-4 py-2 text-sm font-semibold transition hover:bg-white/10 disabled:opacity-30">上一页</button>
  139. <span className="text-sm font-medium text-slate-500">第 <span className="text-white">{page}</span> / {totalPages} 页</span>
  140. <button onClick={() => setPage(p => Math.min(totalPages, p + 1))} disabled={page >= totalPages} className="rounded-xl border border-white/10 bg-white/5 px-4 py-2 text-sm font-semibold transition hover:bg-white/10 disabled:opacity-30">下一页</button>
  141. </div>
  142. )}
  143. </section>
  144. </div>
  145. {/* 黑金风格确认弹窗 */}
  146. {cancelTarget && (
  147. <div className="fixed inset-0 z-50 flex items-center justify-center bg-[#050b14]/80 p-4 backdrop-blur-md">
  148. <div className="w-full max-w-sm overflow-hidden rounded-[2.5rem] border border-white/10 bg-[#0a1120] shadow-2xl">
  149. <div className="p-10 text-center">
  150. <div className="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full bg-rose-500/10 text-rose-400">
  151. <FileText size={32} />
  152. </div>
  153. <h3 className="text-xl font-bold text-white">确认撤销订单?</h3>
  154. <p className="mt-3 text-sm leading-relaxed text-slate-400 px-4">订单号: <span className="text-slate-200">{cancelTarget.serial}</span><br/>撤销后不可恢复。</p>
  155. </div>
  156. <div className="flex border-t border-white/5">
  157. <button onClick={() => setCancelTarget(null)} className="flex-1 py-5 text-sm font-bold text-slate-400 hover:bg-white/5">暂不处理</button>
  158. <div className="w-px bg-white/5" />
  159. <button onClick={handleConfirmCancel} disabled={cancelLoading} className="flex-1 py-5 text-sm font-bold text-rose-400 hover:bg-rose-500/10 disabled:opacity-50">
  160. {cancelLoading ? "执行中..." : "确认撤销"}
  161. </button>
  162. </div>
  163. </div>
  164. </div>
  165. )}
  166. </div>
  167. );
  168. }